/**
 * Provides remote asynchronous service for communicating with the sensorbase and telemetry 
 * analysis service hosts.
 */
package org.hackystat.ui.telemetryviewer.server;

import java.math.BigInteger;
import java.net.URL;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

import javax.xml.datatype.XMLGregorianCalendar;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;

import org.hackystat.dailyprojectdata.client.DailyProjectDataClient;
import org.hackystat.sensorbase.client.SensorBaseClient;
import org.hackystat.sensorbase.client.SensorBaseClientException;
import org.hackystat.sensorbase.resource.projects.jaxb.Project;
import org.hackystat.sensorbase.resource.projects.jaxb.ProjectIndex;
import org.hackystat.sensorbase.resource.projects.jaxb.ProjectRef;
import org.hackystat.sensorbase.resource.projects.jaxb.ProjectSummary;
import org.hackystat.sensorbase.resource.projects.jaxb.Property;
import org.hackystat.telemetry.service.client.TelemetryClient;
import org.hackystat.telemetry.service.resource.chart.jaxb.ParameterDefinition;
import org.hackystat.telemetry.service.resource.chart.jaxb.TelemetryChartData;
import org.hackystat.telemetry.service.resource.chart.jaxb.TelemetryChartDefinition;
import org.hackystat.telemetry.service.resource.chart.jaxb.TelemetryChartIndex;
import org.hackystat.telemetry.service.resource.chart.jaxb.TelemetryChartRef;
import org.hackystat.telemetry.service.resource.chart.jaxb.TelemetryPoint;
import org.hackystat.telemetry.service.resource.chart.jaxb.TelemetryStream;
import org.hackystat.ui.telemetryviewer.client.common.BasicConveyor;
import org.hackystat.ui.telemetryviewer.client.common.DatasetParcel;
import org.hackystat.ui.telemetryviewer.client.common.DatasetConveyor;
import org.hackystat.ui.telemetryviewer.client.common.ProjectParcel;
import org.hackystat.ui.telemetryviewer.client.common.ProjectConveyor;
import org.hackystat.ui.telemetryviewer.client.common.PropertyKeys;
import org.hackystat.ui.telemetryviewer.client.common.RPCException;
import org.hackystat.ui.telemetryviewer.client.common.RPCRemoteService;
import org.hackystat.ui.telemetryviewer.client.common.RelativeDates;
import org.hackystat.ui.telemetryviewer.client.common.Session;
import org.hackystat.utilities.tstamp.Tstamp;
//import org.json.JSONObject;

/**
 * Provides remote asynchronous service for communicating with the sensorbase and telemetry 
 * analysis service hosts.
 * @author David J. Nickles
 */
public class RPCServiceImpl extends RemoteServiceServlet implements RPCRemoteService {

  /* Class variables for serialized object id */
  private static final long serialVersionUID = 11121936L;
  
  /**
   * This method yields a name and general description for each of the available charts.
   * @param session the host and user attributes.
   * @return BasicConveyor carrying the chart names and descriptions.
   */
  public BasicConveyor getCharts(Session session) {
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.start();
    BasicConveyor conveyor = new BasicConveyor();
    ArrayList<ArrayList<String>> chartList = new ArrayList<ArrayList<String>>();
    try {
      TelemetryClient client = new TelemetryClient(session.getTelemetryHost(), 
        session.getUser(), session.getPassword());
      client.authenticate();
      TelemetryChartIndex index = client.getChartIndex();
      for (TelemetryChartRef ref : index.getTelemetryChartRef()) {
        ArrayList<String> chart = new ArrayList<String>();
        TelemetryChartDefinition chartDef = client.getChartDefinition(ref.getName());
        chart.add(chartDef.getName());
        chart.add(chartDef.getDescription());
        chartList.add(chart);
      }
      conveyor.setLists(chartList);
      conveyor.setStatus(Boolean.TRUE.toString());
    } 
    catch (Exception e) {
      log("TV called getCharts method with session: " + session 
        + "\n exception thrown " + e.getMessage());
      conveyor.setStatus(e.getMessage());
    }
    stopwatch.stop();
    log("TV getChart returning " + chartList.size() + " available charts in this service.\n"
      + "exiting method with execution time of " + stopwatch);
    return conveyor; 
  } 
  
  /**
   * Return a detailed description of the chart definition.
   * @param session the host and user attributes.
   * @param chart the name of the requested chart.
   * @return BasicConveyor that contains the chart definition and a status code for the operation.
   */
  public BasicConveyor getChartDefinition(Session session, String chart) {
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.start();
    BasicConveyor conveyor = new BasicConveyor();
    ArrayList<ArrayList<String>> chartDefList = new ArrayList<ArrayList<String>>();
    try {
      TelemetryClient client = new TelemetryClient(session.getTelemetryHost(), 
        session.getUser(), session.getPassword());
      client.authenticate();
      ArrayList<String> basics = new ArrayList<String>();
      TelemetryChartDefinition chartDef = client.getChartDefinition(chart);
      basics.add(chartDef.getName());
      basics.add(chartDef.getDescription());
      basics.add(chartDef.getSourceCode());
      chartDefList.add(basics);
      for (ParameterDefinition paramDef : chartDef.getParameterDefinition()) {
        ArrayList<String> paramList = new ArrayList<String>();
        paramList.add(paramDef.getName());
        paramList.add(paramDef.getDescription());
        paramList.add(paramDef.getType().getName());
        paramList.add(paramDef.getType().getDefault());
        paramList.add(paramDef.getType().getMinValue());
        paramList.add(paramDef.getType().getMaxValue());
        if (paramDef.getType().getValue() != null) {
          for (String value : paramDef.getType().getValue()) {
            paramList.add(value);
          }
        }
        chartDefList.add(paramList);
      }
      conveyor.setStatus(Boolean.TRUE.toString());
    } 
    catch (Exception e) {
      log("TV called getChartDef method with chart: " + chart + "\nsession: " + session 
        + "\nexception thrown: " + e.getMessage());
      conveyor.setStatus(e.getMessage());
    }
    conveyor.setLists(chartDefList);
    stopwatch.stop();
    log("TV getChartDef returning chart " + chart + " with an execution time of " + stopwatch);
    return conveyor; 
  }
  
  /**
   * Return the telemetry data set for a chart service.
   * @param session the host and user attributes.
   * @param chart the name of the requested chart.
   * @param params the list of chart parameters.
   * @param start XML formatted timestamp for beginning of data interval.
   * @param end XML formatted timestamp for end of data interval.
   * @return DatasetConveyor the requested telemetry data.
   */
  public DatasetConveyor getChartData(Session session, String chart, String params, 
    String start, String end) {
    final String granularity = "Day"; 
    String methodParams = "chart: " + chart + ", params: " + params + ", begins " + start 
      + ", ends " + end;
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.start();
    DatasetConveyor conveyor = new DatasetConveyor();
    try {
      SensorBaseClient sbaseClient = new SensorBaseClient(session.getSensorbaseHost(), 
        session.getUser(), session.getPassword());
      sbaseClient.authenticate();
      XMLGregorianCalendar startTstamp = quantize(Tstamp.makeTimestamp());
      XMLGregorianCalendar endTstamp = quantize(Tstamp.makeTimestamp());
      endTstamp = Tstamp.incrementSeconds(endTstamp, -1);
      if (start == null) {
        throw new RPCException("Invalid Parameters: specified start date string was null.");
      }
      else {
        String startTrimed = start.trim();
        if (RelativeDates.LASTDAY.equals(startTrimed)) {
          startTstamp = Tstamp.incrementDays(startTstamp, -1);
        }
        else if (RelativeDates.LASTTWODAYS.equals(startTrimed)) {
          startTstamp = Tstamp.incrementDays(startTstamp, -2);
        }
        else if (RelativeDates.LASTWEEK.equals(startTrimed)) {
          startTstamp = Tstamp.incrementDays(startTstamp, -7);
        }
        else if (RelativeDates.LASTTWOWEEKS.equals(startTrimed)) {
          startTstamp = Tstamp.incrementDays(startTstamp, -14);
        }
        else if (RelativeDates.LASTMONTH.equals(startTrimed)) {
          startTstamp = Tstamp.incrementDays(startTstamp, -28);
        }
        else if (RelativeDates.LASTTWOMONTHS.equals(startTrimed)) {
          startTstamp = Tstamp.incrementDays(startTstamp, -56);
        }
        else {
          startTstamp = Tstamp.makeTimestamp(startTrimed);
          endTstamp = Tstamp.makeTimestamp(end.trim());
        }
      }
      TelemetryClient client = new TelemetryClient(session.getTelemetryHost(), 
        session.getUser(), session.getPassword());
      client.authenticate();
      log("TV getChartData telemetryClient:getChart for date range (" + start + ") " + startTstamp 
        + " to " + endTstamp);
      ArrayList<DatasetParcel> streamArray = new ArrayList<DatasetParcel>();
      StringBuffer messageBuffer = new StringBuffer();
      for (Object obj : session.getProjectList()) {
        if (obj instanceof ProjectParcel) {
          ProjectParcel projectParcel = (ProjectParcel)obj;
          if (projectParcel.isSelected()) {
            TelemetryChartData telemetryData = null;
            if (params == null) {
              telemetryData = client.getChart(chart, 
                projectParcel.getOwner(), projectParcel.getName(), granularity, 
                  startTstamp, endTstamp);
            }
            else {
              telemetryData = client.getChart(chart, projectParcel.getOwner(), 
                projectParcel.getName(), granularity, startTstamp, endTstamp, params);
            }
            for (TelemetryStream stream : telemetryData.getTelemetryStream()) {
              DatasetParcel datasetParcel = new DatasetParcel();
              datasetParcel.setIdentifier(projectParcel.getName() + ' ' + stream.getName());
              datasetParcel.setAxisId(projectParcel.getName() + ' ' + stream.getYAxis().getName());
              datasetParcel.setLabel(projectParcel.getName() + ' ' + stream.getYAxis().getUnits());
              ArrayList<Double> domain = new ArrayList<Double>();
              ArrayList<Double> range = new ArrayList<Double>();
              for (TelemetryPoint point : stream.getTelemetryPoint()) {
                String rangeValue = point.getValue();
                if (rangeValue != null) {
                  // its really important to check that rangeValue is not null, because it often is.
                  Double domainPt = new Double(
                    point.getTime().toGregorianCalendar().getTimeInMillis());
                  domain.add(domainPt);
                  Double rangePt = new Double(Double.parseDouble(rangeValue));
                  range.add(rangePt);
                }
              }
              datasetParcel.setTimeDomain(domain);
              datasetParcel.setRange(range);
              if (domain.isEmpty()) {
                log(projectParcel.getName() + stream.getName() 
                  + " stream contained no data points.");
                messageBuffer.append(projectParcel.getName());
                messageBuffer.append(':');  
                messageBuffer.append(stream.getName());
                messageBuffer.append(" stream contained no data.<br />");   
              }
              else {
                streamArray.add(datasetParcel);
                StringBuffer sBuffer = new StringBuffer(128);
                sBuffer.append(projectParcel.getName());
                sBuffer.append(':');
                sBuffer.append(stream.getName());
                sBuffer.append(" stream contained ");
                sBuffer.append(datasetParcel.getDomain().length);
                sBuffer.append(" XY pair(s).\nAggregated Dataset:\nRange:\n");
                for (Double d : range) {
                  sBuffer.append(d.toString());
                  sBuffer.append(", ");
                }
                sBuffer.append("\nDomain:\n");
                for (Double d : domain) {
                  sBuffer.append(d.toString());
                  sBuffer.append(", ");
                }
                sBuffer.append("\nAdded dataset to parcel.");
                log(sBuffer.toString());
                messageBuffer.append(projectParcel.getName() + ':' + stream.getName() 
                  + " stream contained " + datasetParcel.getDomain().length + " data point");  
                if (domain.size() > 1) {
                  messageBuffer.append('s');  
                }
                messageBuffer.append(".<br />");
              }
            }
          }
        }
      }
      conveyor.setStreams(streamArray);
      conveyor.setMessage(messageBuffer.toString());
      conveyor.setStatus(Boolean.TRUE.toString());
    } 
    catch (Exception e) {
      log("TV called getChartData method for: " + methodParams + "\nsession: " + session 
          + "\nexception thrown: " + e.getMessage());
      conveyor.setStatus(e.getMessage());
    }
    stopwatch.stop();
    log("TV getChartData exiting method with execution time of " + stopwatch + '\n');
    return conveyor; 
  }
  
  /**
   * Returns true if the passed host is a Telemetry Analysis service host.
   * 
   * @param host the URI for the Telemetry host.
   * @return Boolean indicating if the host is a valid Telemetry host.
   */
  public Boolean isTelemetryHost(String host) {
    return TelemetryClient.isHost(host);
  }
  
  /**
   * Returns true if the passed host is a DPD service host.
   * 
   * @param host the URI for the DPD host.
   * @return Boolean indicating if the host is a valid DPD host.
   */
  public Boolean isDPDHost(String host) {
    return DailyProjectDataClient.isHost(host);
  }
  
  /**
   * Returns true if the passed host is a SensorBase service host.
   * 
   * @param host the URI for the SensorBase host.
   * @return Boolean indicating if the host is a valid SensorBase host.
   */
  public Boolean isSensorBaseHost(String host) {
    return SensorBaseClient.isHost(host);
  }

  /**
   * Returns true if the user and password is registered as a user with the SensorBase host.
   * @param session the host and user attributes.
   * @return Boolean indicating if the user and password are valid in the SensorBase
   */
  public Boolean isRegistered(Session session) {
    return SensorBaseClient.isRegistered(session.getSensorbaseHost(), session.getUser(), 
      session.getPassword());
  }

  /**
   * Registers the user with this SensorBase host.
   * 
   * @param host the URI for the SensorBase host.
   * @param user to register.
   * @return Boolean indicating if the registration succeeded with the SensorBase.
   */
  public Boolean registerUser(String host, String user) {
    Boolean result = Boolean.TRUE;
    try {
      SensorBaseClient.registerUser(host, user);
    }
    catch (SensorBaseClientException e) {
      result = Boolean.FALSE;
      log("TV isRegistered, user: " + user + ", error thrown: " + e.getMessage());
    }
    return result;
  }
  
  /**
   * Returns the attributes set in the Manifest file.
   * 
   * @return a list of strings of attribute values from the Manifest file.
   */
  public BasicConveyor getManifest() {
    BasicConveyor conveyor = new BasicConveyor();
    HashMap<String, String> propertyMap = new HashMap<String, String>();
    try {
      URL manifestURL = getServletContext().getResource("/META-INF/MANIFEST.MF");
      if (manifestURL == null) {
        log("TV getManifest, unable to locate manifest.");
        conveyor.setStatus("Unable to locate manifest.");
      }
      else {
        Manifest mf = new Manifest(manifestURL.openStream());
        Attributes attr = mf.getMainAttributes();
        String title = attr.getValue(PropertyKeys.TITLE);
        if (title == null) {
          log("TV unable to get 'Implementation-Title' attribute from the Manifest.");
          title = "Hackystat Developer Widget";
        }
        propertyMap.put(PropertyKeys.TITLE, title);
        String version = attr.getValue(PropertyKeys.VERSION);
        if (version == null) {
          log("TV unable to get 'Implementation-Version' attribute from the Manifest.");
          version = "Development";
        }
        propertyMap.put(PropertyKeys.VERSION, version);
        String vendor = attr.getValue(PropertyKeys.VENDOR);
        if (vendor == null) {
          log("TV unable to get 'Implementation-Version' attribute from the Manifest.");
          vendor = "CSDL @ UH";
        }
        propertyMap.put(PropertyKeys.VENDOR, vendor);
        conveyor.setMap(propertyMap);
        conveyor.setStatus(Boolean.TRUE.toString());
      }
    } 
    catch (Exception e) {
      log("TV getManifest, exception thrown: " + e.getMessage());
      conveyor.setStatus(e.getMessage());
    }
    return conveyor; 
  }
  
  /**
   * This method retrieves the host uris for the Sensorbase and Telemetry services from the
   * Telemetry Viewer properties file.
   * 
   * @return BasicConveyor carrying the host uris for the Sensorbase and Telemetry services from
   * the Telemetry Viewer properties file.
   */
  public BasicConveyor getHosts() {
    BasicConveyor conveyor = new BasicConveyor();
    HashMap<String, String> propertyMap = new HashMap<String, String>();
    try {
      Properties telemetryViewerProperties = new Properties();
      if (telemetryViewerProperties == null) {
        log("TV getHosts, Unable to locate TelemetryViewer properties file.");
        conveyor.setStatus("Unable to locate properties file.");
      }
      else {
        propertyMap.put(PropertyKeys.SENSORBASE_HOST, 
          telemetryViewerProperties.getSensorbaseHost());
        propertyMap.put(PropertyKeys.TELEMETRY_HOST, 
          telemetryViewerProperties.getTelemetryHost());
        conveyor.setMap(propertyMap);
        conveyor.setStatus(Boolean.TRUE.toString());
      }
    } 
    catch (Exception e) {
      log("TV getHosts, exception thrown: " + e.getMessage());
      conveyor.setStatus(e.getMessage());
    }
    return conveyor; 
  }
  
  /**
   * Zeros the hour.minute.second.millisecond for this timestamp.
   * @param cal timestamp as XMLGregorianCalendar object
   * @return the quantized timestamp as an XMLGregorianCalendar object
   */
  private XMLGregorianCalendar quantize(XMLGregorianCalendar cal) {
    cal.setHour(0);
    cal.setMinute(0);
    cal.setSecond(0);
    cal.setMillisecond(0);
    return cal;
  }
  
  /**
   * Computes the first day of the given month.
   * Zeros the hour.minute.second.millisecond for this timestamp.
   * @param year the number of the year as an integer
   * @param month the number of the month as an integer
   * @return the first of the month timestamp as an XMLGregorianCalendar object
   */
  private XMLGregorianCalendar getFirstOfMonth(int year, int month) {
    XMLGregorianCalendar cal = Tstamp.makeTimestamp();
    cal.setYear(year);
    cal.setMonth(month);
    cal.setDay(1);
    cal = quantize(cal);
    return cal;
  }
  
  /**
   * Computes the first day of the next month.
   * Zeros the hour.minute.second.millisecond for this timestamp.
   * @param year the number of the year as an integer
   * @param month the number of the month as an integer
   * @return the first of the month timestamp as an XMLGregorianCalendar object
   */
  private XMLGregorianCalendar getFirstOfNextMonth(int year, int month) {
    XMLGregorianCalendar cal = Tstamp.makeTimestamp();
    int nextMonth = ((month % 12) + 1); 
    int nextYear = year;
    if (nextMonth == 1) {
      nextYear++;
    }
    cal.setYear(nextYear);
    cal.setMonth(nextMonth);
    cal.setDay(1);
    cal = quantize(cal);
    return cal;
  }

  /**
   * Return a formatted description of the requested sensor data on the specified interval.
   * @param session the host and user attributes.
   * @param year of interval
   * @param month of interval
   * @return Boolean array of days with data
   */
  public ArrayList<Boolean> getOverview(Session session, int year, int month) {
    log("TV getOverview with session: " + session);
    XMLGregorianCalendar start = getFirstOfMonth(year, month);
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.start();  
    ArrayList<Boolean> dayList = new ArrayList<Boolean>();
    GregorianCalendar cal = start.toGregorianCalendar();
    int totalDays = cal.getActualMaximum(GregorianCalendar.DAY_OF_MONTH);
    log("TV getOverview for " + month + "/" + year + ", #days: " + totalDays);
    SensorBaseClient client = 
      new SensorBaseClient(session.getSensorbaseHost(), session.getUser(), session.getPassword());
    for (int i = 0; i < totalDays; i++) {
      XMLGregorianCalendar end = Tstamp.incrementSeconds(Tstamp.incrementDays(start, 1), -1);
      ProjectSummary summary = null;
      try {
        summary = client.getProjectSummary(session.getUser(), ProjectParcel.DEFAULT, start, end);
      }
      catch (SensorBaseClientException e) {
        log("TV: getOverview: " + e.getMessage() + ", occured calling projectSummary for period " 
          + start.toXMLFormat() + " to " + end.toXMLFormat());
      }
      if (summary == null) {
        dayList.add(i, Boolean.FALSE);
      }
      else if (summary.getSensorDataSummaries().getNumInstances().equals(BigInteger.ZERO)) {
        dayList.add(i, Boolean.FALSE);
      }
      else {
        dayList.add(i, Boolean.TRUE);
      }
      start = Tstamp.incrementDays(start, 1);
    }
    stopwatch.stop();
    log("TV method getOverview exiting method with execution time of " + stopwatch + '\n');
    return dayList; 
  }

  /**
   * Returns the index of all Projects from this server for this user.
   * @param session host and user information.
   * @return Returns the index of all Projects from this server for this user.
   */
  public ProjectConveyor getProjects(Session session) {
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.start();  
    ProjectConveyor conveyor = new ProjectConveyor();
    ArrayList<ProjectParcel> parcelList = new ArrayList<ProjectParcel>();
    try {
      SensorBaseClient client = new SensorBaseClient(session.getSensorbaseHost(), 
        session.getUser(), session.getPassword());
      ProjectIndex index = client.getProjectIndex(session.getUser());
      for (ProjectRef ref : index.getProjectRef()) {
        Project p = client.getProject(ref);
        ProjectParcel projectParcel = new ProjectParcel();
        projectParcel.setName(p.getName());
        projectParcel.setDescription(p.getDescription());
        projectParcel.setOwner(p.getOwner());
        projectParcel.setMembers(p.getMembers().getMember());
        projectParcel.setInvitations(p.getInvitations().getInvitation());
        HashMap<String, String> propertyMap = new HashMap<String, String>();
        for (Property property : p.getProperties().getProperty()) {
          propertyMap.put(property.getKey(), property.getValue());
        }
        projectParcel.setProperties(propertyMap);
        projectParcel.setUriPatterns(p.getUriPatterns().getUriPattern());
        projectParcel.setStartTime(SimpleDateFactories.getInstance().getDatefactory().format(
          p.getStartTime().toGregorianCalendar().getTime()));
        projectParcel.setEndTime(SimpleDateFactories.getInstance().getDatefactory().format(
          p.getEndTime().toGregorianCalendar().getTime()));
        parcelList.add(projectParcel);
      }
      conveyor.setStatus(Boolean.TRUE.toString());
    } 
    catch (Exception e) {
      log("TV getProjectParcels method with session: " + session
          + "\nexception thrown: " + e.getMessage());
      conveyor.setStatus(e.getMessage());
    }
    conveyor.setProjects(parcelList);
    stopwatch.stop();
    log("TV getProjectParcels exiting with execution time of " + stopwatch + '\n');
    return conveyor; 
  }

  /**
   * Logs the error message from the javascript client.
   * 
   * @param text message to log.
   */
  public void logClient(String text) {
    log("TV client log message: " + text);
  }
}